/**
 * Copyright (C) 2009 Noel Ang
 *
 * This file is part of Samsara.
 *
 * Samsara is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Samsara is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Samsara.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/
package com.lunarshades.samsara.ui;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import javax.swing.Action;
import javax.swing.ListSelectionModel;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

/**
 * A ListSelectionModel implementation that maintains transient and
 * non-transient selection intervals.
 * <p/>
 * Any interval selection communicated to the model via {@link
 * #setSelectionInterval(int, int)} is considered a transient selection, one
 * that is deselected (removed) by a subsequent call to this same method.  All
 * other intervals created thru other methods are considered non-transient, and
 * are only deselected, either partially or completely, by {@link
 * #removeIndexInterval(int, int)} or {@link #removeSelectionInterval(int, int)}
 * messages with intersecting intervals, or by {@link #clearSelection()} calls.
 * <p/>
 * The effect of this implementation is to permit the host controller/view to
 * manage/render a "mobile" selection separate from a set of static selections.
 * It permits "linear" input methods (e.g., keyboard-based list navigation) to
 * maintain multiple list selections and still traverse the list, without having
 * to keep key modifiers active.
 *
 * @author Noel Ang, sparta.variable@gmail.com
 */
class RosterRowSelectionModel implements ListSelectionModel {

RosterRowSelectionModel() {
    mSelections = Collections.synchronizedList(new LinkedList<Selection>());
    mListeners =
            Collections.synchronizedSet(new HashSet<ListSelectionListener>());
}

private int[] order(int... indices) {
    assert indices != null && indices.length > 0;
    Arrays.sort(indices);
    return indices;
}

/**
 * Determines if the interval determined by index ([index, index]) is currently
 * a non-transient selection in the model.
 *
 * @param index Starting and ending index of the interval in regard.
 *
 * @return True if the interval is a non-transient selection in the model.
 */
protected boolean isSelectedIndexNonTransient(int index) {
    assert 0 <= index;
    boolean isSelected = false;
    Selection selected = new Selection(index, index);
    synchronized (mSelections) {
        for (Selection s : mSelections) {
            if ((isSelected = (s.contains(selected)
                    && !(s instanceof TransientSelection)))) {
                break;
            }
        }
    }
    return isSelected;
}

/**
 * Selects the interval specified by [index0, index1] in the model. The
 * selection is considered transient, meaning it replaces existing transient
 * selections, if any, in the model.
 *
 * @param index0 Selection interval starting index, inclusive.
 * @param index1 Selection interval ending index, inclusive.
 */
protected void updateTransientSelection(int index0, int index1) {
    assert 0 <= index0 && index0 <= index1;
    Selection current = null;
    List<Selection> removed = new LinkedList<Selection>();
    synchronized (mSelections) {
        boolean gotMatch = false;
        if (!mSelections.isEmpty()) {
            ListIterator<Selection> iter = mSelections.listIterator();
            while (iter.hasNext()) {
                Selection s = iter.next();
                if (s instanceof TransientSelection) {
                    // Remove old transients except for single one that can
                    // can substitute for new transient, if present
                    if (!gotMatch) {
                        gotMatch = (index0 == s.index0 && index1 == s.index1);
                    }
                    if (!gotMatch) {
                        iter.remove();
                        removed.add(s);
                    }
                }
            }
        }
        if (!gotMatch) {
            current = new TransientSelection(index0, index1);
            mSelections.add(current);
        }
        Collections.sort(mSelections);
    }
    for (Selection s : removed) {
        fireListSelectionEvent(s);
    }
    if (current != null) {
        fireListSelectionEvent(current);
    }
}

/**
 * Selects the interval specified by [index0, index1] in the model. The
 * selection is considered non-transient, meaning does not affect any transient
 * selections in the model, and they don't affect it.
 *
 * @param index0 Selection interval starting index, inclusive.
 * @param index1 Selection interval ending index, inclusive.
 */
protected void updateSelection(int index0, int index1) {
    assert 0 <= index0 && index0 <= index1;
    List<Selection> coverage = new LinkedList<Selection>();
    List<Selection> gaps = null;
    Selection selection;
    synchronized (mSelections) {
        selection = new Selection(index0, index1);
        int initialSize = mSelections.size();
        for (Selection s : mSelections) {
            if (!(s instanceof TransientSelection)) {
                Selection overlap;
                if ((overlap = s.intersection(selection)) != null) {
                    if (overlap.length <= selection.length) {
                        coverage.add(overlap);
                    }
                }
            }
        }
        if (coverage.isEmpty()) {
            mSelections.add(selection);
        } else {
            for (Selection overlap : coverage) {
                gaps = overlap.complement(selection);
                for (Selection slice : gaps) {
                    mSelections.add(slice);
                }
            }
        }
        if (initialSize != mSelections.size()) {
            Collections.sort(mSelections);
        }
    }
    if (coverage.isEmpty()) {
        fireListSelectionEvent(selection);
    } else if (gaps != null) {
        for (Selection slice : gaps) {
            fireListSelectionEvent(slice);
        }
    }
}

/**
 * Deselects the interval specified by [index0, index1] in the model.
 *
 * @param index0 Selection interval starting index, inclusive.
 * @param index1 Selection interval ending index, inclusive.
 */
protected void updateUnselection(int index0, int index1) {
    assert 0 <= index0 && index0 <= index1;
    List<Selection> coverage = new LinkedList<Selection>();
    List<Selection> removed = new LinkedList<Selection>();
    synchronized (mSelections) {
        Selection unselection = new Selection(index0, index1);
        ListIterator<Selection> iter = mSelections.listIterator();
        while (iter.hasNext()) {
            Selection s = iter.next();
            Selection overlap;
            if (!(s instanceof TransientSelection)) {
                if ((overlap = s.intersection(unselection)) != null) {
                    iter.remove();
                    removed.add(s);
                    coverage.addAll(overlap.complement(s));
                }
            }
        }
        for (Selection slice : coverage) {
            mSelections.add(slice);
        }
        Collections.sort(mSelections);
    }
    for (Selection s : removed) {
        fireListSelectionEvent(s);
    }
    for (Selection s : coverage) {
        fireListSelectionEvent(s);
    }
}

/**
 * Used to dispatch a ListSelectionEvent at the conclusion of a sequence of
 * selection events intended to be processed as a whole, that characterizes the
 * entire series.
 *
 * @param isValueAdjusting Must be false or method is no-op.
 *
 * @see {@link #setValueIsAdjusting(boolean)}
 */
protected void fireListSelectionEvent(boolean isValueAdjusting) {
    if (!isValueAdjusting) {
        int start = mFirstChangedIndex;
        int end = mLastChangedIndex;
        if (start <= end) {
            Selection s = new Selection(start, end);
            mFirstChangedIndex = Integer.MAX_VALUE;
            mLastChangedIndex = -1;
            fireListSelectionEvent(s);
        }
    }
}

/**
 * Used to notify registered ListSelectionListeners of selection changes.
 *
 * @param s Selection interval whose status has changed.
 */
protected void fireListSelectionEvent(Selection s) {
    boolean isValueAdjusting = getValueIsAdjusting();
    if (isValueAdjusting) {
        synchronized (this) {
            mFirstChangedIndex = Math.min(mFirstChangedIndex, s.index0);
            mLastChangedIndex = Math.max(mLastChangedIndex, s.index1);
        }
    }

    final ListSelectionEvent EVENT =
            new ListSelectionEvent(this, s.index0, s.index1, isValueAdjusting);
    synchronized (mListeners) {
        for (ListSelectionListener l : mListeners) {
            l.valueChanged(EVENT);
        }
    }
}

/**
 * Invert the state of the non-transient selection interval denoted by [index0,
 * index1]. {@code index0} doesn't have to be less than or equal to {@code
 * index1}. If the interval is selected in the model, it is deselected, and
 * vice-versa. If the selection interval contains subsets of transient intervals
 * (including proper subsets), those transient intervals are excluded from
 * inversion; they are not subject to deselection.
 *
 * @param index0 One end of the interval, inclusive.
 * @param index1 Other end of the interval, inclusive.
 */
void toggleSelectionInterval(int index0, int index1) {
    int[] indices = order(index0, index1);
    int start = indices[0];
    int end = indices[1];
    if (0 <= start) {
        for (int i = start; i <= end; ++i) {
            if (isSelectedIndexNonTransient(i)) {
                removeIndexInterval(i, i);
            } else {
                addSelectionInterval(i, i);
            }
        }
        setSelectionInterval(index0, index1);
    }
}

/**
 * Changes the selection to be between {@code index0} and {@code index1}
 * inclusive. {@code index0} doesn't have to be less than or equal to {@code
 * index1}.
 * <p/>
 * If this represents a change to the current selection, then each {@code
 * ListSelectionListener} is notified of the change.
 *
 * @param index0 one end of the interval.
 * @param index1 other end of the interval
 *
 * @see #addListSelectionListener
 */
public void setSelectionInterval(int index0, int index1) {
    mAnchorIndex = index0;
    mLeadIndex = index1;
    int[] indices = order(index0, index1);
    index0 = indices[0];
    index1 = indices[1];
    if (0 <= index0) {
        updateTransientSelection(index0, index1);
    }
}

/**
 * Changes the selection to be the set union of the current selection and the
 * indices between {@code index0} and {@code index1} inclusive. {@code index0}
 * doesn't have to be less than or equal to {@code index1}.
 * <p/>
 * If this represents a change to the current selection, then each {@code
 * ListSelectionListener} is notified of the change.
 *
 * @param index0 one end of the interval.
 * @param index1 other end of the interval
 *
 * @see #addListSelectionListener
 * @see #setSelectionInterval
 */
public void addSelectionInterval(int index0, int index1) {
    mAnchorIndex = index0;
    mLeadIndex = index1;
    int[] indices = order(index0, index1);
    index0 = indices[0];
    index1 = indices[1];
    if (0 <= index0) {
        updateSelection(index0, index1);
    }
}

/**
 * Changes the selection to be the set difference of the current selection and
 * the indices between {@code index0} and {@code index1} inclusive. {@code
 * index0} doesn't have to be less than or equal to {@code index1}.
 * <p/>
 * If this represents a change to the current selection, then each {@code
 * ListSelectionListener} is notified of the change.
 *
 * @param index0 one end of the interval.
 * @param index1 other end of the interval
 *
 * @see #addListSelectionListener
 */
public void removeSelectionInterval(int index0, int index1) {
    mAnchorIndex = index0;
    mLeadIndex = index1;
    int[] indices = order(index0, index1);
    index0 = indices[0];
    index1 = indices[1];
    if (0 <= index0) {
        updateUnselection(index0, index1);
    }
}

/**
 * Returns the first selected index or -1 if the selection is empty.
 */
public int getMinSelectionIndex() {
    synchronized (mSelections) {
        return !mSelections.isEmpty() ? mSelections.get(0).index0 : -1;
    }
}

/**
 * Returns the last selected index or -1 if the selection is empty.
 */
public int getMaxSelectionIndex() {
    synchronized (mSelections) {
        return !mSelections.isEmpty() ? mSelections.get(mSelections.size() - 1
        ).index1 : -1;
    }
}

/**
 * Returns true if the specified index is selected.
 */
public boolean isSelectedIndex(int index) {
    boolean isSelected = false;
    Selection selected = new Selection(index, index);
    synchronized (mSelections) {
        for (Selection s : mSelections) {
            if ((isSelected = s.contains(selected))) {
                break;
            }
        }
    }
    return isSelected;
}

/**
 * Return the first index argument from the most recent call to
 * setSelectionInterval(), addSelectionInterval() or removeSelectionInterval().
 * The most recent index0 is considered the "anchor" and the most recent index1
 * is considered the "lead".  Some interfaces display these indices specially,
 * e.g. Windows95 displays the lead index with a dotted yellow outline.
 *
 * @see #getLeadSelectionIndex
 * @see #setSelectionInterval
 * @see #addSelectionInterval
 */
public int getAnchorSelectionIndex() {
    return mAnchorIndex;
}

/**
 * Set the anchor selection index.
 *
 * @see #getAnchorSelectionIndex
 */
public void setAnchorSelectionIndex(int index) {
    mAnchorIndex = index;
}

/**
 * Return the second index argument from the most recent call to
 * setSelectionInterval(), addSelectionInterval() or removeSelectionInterval().
 *
 * @see #getAnchorSelectionIndex
 * @see #setSelectionInterval
 * @see #addSelectionInterval
 */
public int getLeadSelectionIndex() {
    return mLeadIndex;
}

/**
 * Set the lead selection index.
 *
 * @see #getLeadSelectionIndex
 */
public void setLeadSelectionIndex(int index) {
    mLeadIndex = index;
}

/**
 * Change the selection to the empty set.  If this represents a change to the
 * current selection then notify each ListSelectionListener.
 *
 * @see #addListSelectionListener
 */
public void clearSelection() {
    List<Selection> selections;
    synchronized (mSelections) {
        selections = new LinkedList<Selection>(mSelections);
        mSelections.clear();
    }
    for (Selection s : selections) {
        fireListSelectionEvent(s);
    }
}

/**
 * Returns true if no indices are selected.
 */
public boolean isSelectionEmpty() {
    return mSelections.isEmpty();
}

/**
 * Insert length indices beginning before/after index.  This is typically called
 * to sync the selection model with a corresponding change in the data model.
 */
public void insertIndexInterval(int index, int length, boolean before) {
    final int START = before ? Math.max(0, index - length) : index + 1;
    final int END = before
                    ? Math.min(index - 1, START + length - 1)
                    : START + length - 1;
    for (int i = START; i <= END; ++i) {
        Selection s = new Selection(i, i);
        mSelections.add(s);
    }
    if (START <= END) {
        fireListSelectionEvent(new Selection(START, END));
    }
}

/**
 * Remove the indices in the interval index0,index1 (inclusive) from the
 * selection model.  This is typically called to sync the selection model width
 * a corresponding change in the data model.
 */
public void removeIndexInterval(int index0, int index1) {
    int[] indices = order(index0, index1);
    index0 = indices[0];
    index1 = indices[1];
    updateUnselection(index0, index1);
}

/**
 * Sets the {@code valueIsAdjusting} property, which indicates whether or not
 * upcoming selection changes should be considered part of a single change. The
 * value of this property is used to initialize the {@code valueIsAdjusting}
 * property of the {@code ListSelectionEvent}s that are generated.
 * <p/>
 * For example, if the selection is being updated in response to a user drag,
 * this property can be set to {@code true} when the drag is initiated and set
 * to {@code false} when the drag is finished. During the drag, listeners
 * receive events with a {@code valueIsAdjusting} property set to {@code true}.
 * At the end of the drag, when the change is finalized, listeners receive an
 * event with the value set to {@code false}. Listeners can use this pattern if
 * they wish to update only when a change has been finalized.
 * <p/>
 * Setting this property to {@code true} begins a series of changes that is to
 * be considered part of a single change. When the property is changed back to
 * {@code false}, an event is sent out characterizing the entire selection
 * change (if there was one), with the event's {@code valueIsAdjusting} property
 * set to {@code false}.
 *
 * @param valueIsAdjusting the new value of the property
 *
 * @see #getValueIsAdjusting
 * @see ListSelectionEvent#getValueIsAdjusting
 */
public void setValueIsAdjusting(boolean valueIsAdjusting) {
    boolean valueWasAdjusting = mIsValueAdjusting;
    mIsValueAdjusting = valueIsAdjusting;
    if (valueWasAdjusting && !valueIsAdjusting) {
        fireListSelectionEvent(valueIsAdjusting);
    }
}

/**
 * Returns {@code true} if the selection is undergoing a series of changes.
 *
 * @return true if the selection is undergoing a series of changes
 *
 * @see #setValueIsAdjusting
 */
public boolean getValueIsAdjusting() {
    return mIsValueAdjusting;
}

/**
 * Sets the selection mode. The following list describes the accepted selection
 * modes: <ul> <li>{@code ListSelectionModel.SINGLE_SELECTION} - Only one list
 * index can be selected at a time. In this mode, {@code setSelectionInterval}
 * and {@code addSelectionInterval} are equivalent, both replacing the current
 * selection with the index represented by the second argument (the "lead").
 * <li>{@code ListSelectionModel.SINGLE_INTERVAL_SELECTION} - Only one
 * contiguous interval can be selected at a time. In this mode, {@code
 * addSelectionInterval} behaves like {@code setSelectionInterval} (replacing
 * the current selection), unless the given interval is immediately adjacent to
 * or overlaps the existing selection, and can therefore be used to grow it.
 * <li>{@code ListSelectionModel.MULTIPLE_INTERVAL_SELECTION} - In this mode,
 * there's no restriction on what can be selected. </ul>
 *
 * @throws IllegalArgumentException if the selection mode isn't one of those
 *                                  allowed
 * @see #getSelectionMode
 */
public void setSelectionMode(int selectionMode) {
    if (selectionMode != MULTIPLE_INTERVAL_SELECTION) {
        throw new IllegalArgumentException(
                "Only MULTIPLE_INTERVAL_SELECTION mode supported."
        );
    }
}

/**
 * Returns the current selection mode.
 *
 * @return the current selection mode
 *
 * @see #setSelectionMode
 */
public int getSelectionMode() {
    return ListSelectionModel.MULTIPLE_INTERVAL_SELECTION;
}

/**
 * Add a listener to the list that's notified each time a change to the
 * selection occurs.
 *
 * @param x the ListSelectionListener
 *
 * @see #removeListSelectionListener
 * @see #setSelectionInterval
 * @see #addSelectionInterval
 * @see #removeSelectionInterval
 * @see #clearSelection
 * @see #insertIndexInterval
 * @see #removeIndexInterval
 */
public void addListSelectionListener(ListSelectionListener x) {
    mListeners.add(x);
}

/**
 * Remove a listener from the list that's notified each time a change to the
 * selection occurs.
 *
 * @param x the ListSelectionListener
 *
 * @see #addListSelectionListener
 */
public void removeListSelectionListener(ListSelectionListener x) {
    mListeners.remove(x);
}

/**
 * Describes a transient selection interval.
 */
static class TransientSelection extends Selection {
    TransientSelection(int index0, int index1) {
        super(index0, index1);
    }
}

/**
 * Describes a selection interval.
 */
static class Selection implements Comparable<Selection> {
    Selection(int index0, int index1) {
        this.index0 = Math.min(index0, index1);
        this.index1 = Math.max(index0, index1);
        assert this.index0 <= this.index1;
        length = this.index1 - this.index0 + 1;
        assert length >= 1;
    }

    public int compareTo(Selection o) {
        if (o == null) {
            return 1;
        }
        if (this == o) {
            return 0;
        }
        return index0 - o.index0;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Selection selection = (Selection) o;
        return index0 == selection.index0 && index1 == selection.index1;

    }

    @Override
    public int hashCode() {
        int result = index0;
        result = 31 * result + index1;
        result = 31 * result;
        return result;
    }

    public boolean contains(Selection s) {
        return (index0 <= s.index0 && s.index1 <= index1);
    }

    /**
     * Computes the relative complement of this selection relative to s.
     *
     * @param s A selection
     *
     * @return The set of selections (as a list) that are members of s, but not
     *         of this selection.
     *
     * @throws NullPointerException if s is null.
     */
    public List<Selection> complement(Selection s) {
        if (s == null) {
            throw new NullPointerException();
        }
        List<Selection> complements = new LinkedList<Selection>();
        int exclusionStart = index0;
        int exclusionEnd = index1;
        if (s.index0 < exclusionStart) {
            int end =
                    Math.max(s.index0, Math.min(s.index1, exclusionStart - 1));
            complements.add(new Selection(s.index0, end));
        }
        if (exclusionEnd < s.index1) {
            int start =
                    Math.min(s.index1, Math.max(s.index0, exclusionEnd + 1));
            complements.add(new Selection(start, s.index1));
        }
        return complements;
    }

    /**
     * Computes the intersection of this selection to s.
     *
     * @param s A selection
     *
     * @return The set of selections indices (as a list of selections) that are
     *         members of this selection and s, or null if this selection and s
     *         do not intersect.
     */
    public Selection intersection(Selection s) {
        if (s != null) {
            int start = Math.max(index0, s.index0);
            int end = Math.min(index1, s.index1);
            s = (start <= end ? new Selection(start, end) : null);
        }
        return s;
    }

    private final int index0;
    private final int index1;
    private final int length;
}

final Action ADD_TO_SELECTION_ACTION = new RosterSelectToggle(this);

private final Set<ListSelectionListener> mListeners;
private final List<Selection> mSelections;
private volatile boolean mIsValueAdjusting = false;
private volatile int mLeadIndex = -1;
private volatile int mAnchorIndex = -1;
private volatile int mFirstChangedIndex = Integer.MAX_VALUE;
private volatile int mLastChangedIndex = -1;
}
